java基础thread——java5之后的多线程(浅尝辄止)

承上启下

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。

一、JDK5中Lock锁的使用

void lock() 上锁

void unlock() 释放锁

代码示意:

public class SellTicket implements Runnable {
    private int ticket = 20;
    private Lock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true){
                lock.lock();
                if (ticket <= 0) {
                    break;
                }
                //卖票这个动作不安全。
                System.out.println(Thread.currentThread().getName() + "正在售卖第" + (ticket--) + "票");
                lock.unlock();
                System.out.println(Thread.currentThread().getName()+"结束");
            }

    }
}

首先我们要造一个锁

Lock lock = new ReentrantLock();

然后调用lock.lock()和lock.unlock()将需要上锁的代码包起来。

但是查看java的一些源码,还是synchronized用的多。

虽然线程有了锁解决了安全问题,但是偶尔也会因为失误操作出现死锁的情况。

同步弊端:

  • 效率低
  • 如果出现了同步嵌套,就容易产生死锁问题

什么是死锁:

是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象。

示例:

首先造两个锁:

public class MyLock {
    // 创建两把锁对象
    public static final Object objA = new Object();
    public static final Object objB = new Object();
}

同步代码块嵌套:

public class DieLock extends Thread {
    private boolean flag;
    public DieLock(boolean flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
        if (flag) {
            synchronized (MyLock.objA) {
                System.out.println("if objA");
                synchronized (MyLock.objB) {
                    System.out.println("if objB");
                }
            }
        } else {
            synchronized (MyLock.objB) {
                System.out.println("else objB");
                synchronized (MyLock.objA) {
                    System.out.println("else objA");
                }
            }
        }
    }
}

测试:

public class DieLockDemo {
    public static void main(String[] args) {
        DieLock dl1 = new DieLock(true);
        DieLock dl2 = new DieLock(false);

        dl1.start();
        dl2.start();
    }
}

运行打印:
if objA
else objB

二、线程间通信

生产者、消费者模式:

生产者没有就生产,有就等待消费者消费;消费者有就消费,没有就等待生产者生产。

java提供了等待唤醒的机制。

Object类中提供了三个方法:

wait():等待

notify():唤醒单个线程

notifyAll():唤醒所有线程

代码示例:

public class Student {
    String name;
    int age;
    boolean flag;
}

生产者:

public class SetThread implements Runnable {
    private Student s;
    public SetThread(Student s){
        this.s = s;
    }
    private int x = 0;
    @Override
    public void run() {
        while (true){
            synchronized (s) {
                if (s.flag){
                    try {
                        s.wait();//t1等着,释放锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if (x % 2 == 0) {
                    s.age=20;
                    s.name="徐繁韵";
                } else {
                    s.age=21;
                    s.name="唐富平";
                }
                x++;
                s.flag=true;
                s.notify();//唤醒t2,唤醒并不表示你立马可以执行,必须还得抢CPU的执行权。
            }
            //t1有,或者t2有
        }
    }
}

消费者:

public class GetThread implements Runnable {
    private Student s;
    public GetThread(Student s){
        this.s = s;
    }
    @Override
    public void run() {
        while (true){
            synchronized (s) {
                if (!s.flag){
                    try {
                        s.wait();//t2等待,立即释放锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(s.name + ":" + s.age);
                s.flag=false;
                s.notify();//唤醒t1,唤醒并不表示你立马可以执行,必须还得抢CPU的执行权。
            }
        }

    }
}

测试:

public class StudentDemo {
    public static void main(String[] args) {
        Student s = new Student();
        SetThread st = new SetThread(s);
        GetThread gt = new GetThread(s);
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(gt);
        t1.start();
        t2.start();
    }
}

输出打印:
徐繁韵:20
唐富平:21
徐繁韵:20
唐富平:21
徐繁韵:20
唐富平:21
徐繁韵:20
唐富平:21
徐繁韵:20
。
。
。

看的出是生产一条消费一条。为了实现线程间的通信,将共同操作的数据通过有参构造器传入线程。

思考一个问题,为什么等待唤醒的方法不定义在Thread里呢?

这些方法的调用必须通过锁对象调用,而我们刚才使用的锁对象是任意锁对象。所以,这些方法必须定义在Object类中。

栗子优化:

既然wait()、notify()、notifyAll()定义在锁对象里,那么我们把前面的栗子优化一下。

把Student的成员变量给私有化,把设置和获取的操作给封装成功能,并加上同步。设置或者获取的线程里面只需要调用方法即可。

public class Student {
    private String name;
    private int age;
    boolean flag;

    public synchronized  void  set(String name,int age){
        if (flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.name = name;
        this.age = age;
        this.flag = true;
        this.notify();
    }

    public synchronized  void get(){
        if (!this.flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(this.name+":"+this.age);
        this.flag = false;
        this.notify();
    }
}


public class SetThread implements Runnable {
    private Student s;
    public SetThread(Student s){
        this.s = s;
    }
    private int x = 0;
    @Override
    public void run() {
        while (true){
                if (x % 2 == 0) {
                   s.set("徐繁韵",20);
                } else {
                   s.set("唐富平",21);
                }
                x++;
            }
        }
}


public class GetThread implements Runnable {
    private Student s;
    public GetThread(Student s){
        this.s = s;
    }
    @Override
    public void run() {
        while (true){
            s.get();
        }
    }
}

public class StudentDemo {
    public static void main(String[] args) {
        Student s = new Student();
        SetThread st = new SetThread(s);
        GetThread gt = new GetThread(s);
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(gt);
        t1.start();
        t2.start();
    }
}

线程的状态转换图:

status.png

三、线程组:

Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。

查看我们平时创建的线程默认是属于哪个组:

        MyRunnable my = new MyRunnable();
        Thread t1 = new Thread(my,"线程一");
        Thread t2 = new Thread(my,"线程二");
        System.out.println(t1.getThreadGroup().getName());//main
        System.out.println(t2.getThreadGroup().getName());//main
        System.out.println(Thread.currentThread().getThreadGroup().getName());//main

可以看出主线程和我们创建的线程都默认属于main线程组。

接下来我们自定义线程组:

        //创建一个线程组
        ThreadGroup tg = new ThreadGroup("dev");
        MyRunnable my = new MyRunnable();
        创建线程时分配组
        Thread t1 = new Thread(tg,my,"线程一");
        Thread t2 = new Thread(tg,my,"线程二");
        System.out.println(t1.getThreadGroup().getName());//dev
        System.out.println(t2.getThreadGroup().getName());//dev

线程组可以统一管理:

        tg.setDaemon(true);
        tg.interrupt();
        tg.destroy();
        tg.isDestroyed();
        tg.isDaemon();

四、线程池:

程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。

线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。

在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池

JDK5新增了一个Executors工厂类来产生线程池

有如下几个方法:

public static ExecutorService newCachedThreadPool()
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newSingleThreadExecutor()

这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。它提供了如下方法:

Future<?> submit(Runnable task)

<T> Future<T> submit(Callable<T> task)

代码示意:

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i <10 ; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }
    }
}

public class ThreadPoolDemo {
    public static void main(String[] args) {
        //创建线程池,大小为2
        ExecutorService pool = Executors.newFixedThreadPool(2);
        //将实现了接口Runnable的线程放到线程池里运行
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        pool.shutdown();//关闭线程池

    }
}

因为线程池的出现,实现线程的方式有了第三种。

实现Callable接口

public class MyCallable implements Callable {
    private String name;

    public MyCallable(String name){
        this.name = name ;
    }
    @Override
    public Object call() throws Exception {
        Thread.currentThread().setName(this.name);
        for (int i = 0; i <10 ; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }
        return null;
    }
}

public class ThreadCallableDemo {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        pool.submit(new MyCallable("call一号"));
        pool.submit(new MyCallable("call二号"));
        pool.shutdown();

    }
}

这种方式必须依赖线程池实现。可以看出在线程池中,Runnable和Callable两种方式基本相同,不同的是Callable接口是支持泛型的,call()也是有返回值,返回值类型是泛型的类型。

简单应用:

//计算1-n之后
public class MyCallable implements Callable<Integer> {
    private Integer num;
    public MyCallable(Integer num){
        this.num = num;
    }
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <=num; i++) {
            sum +=i;
        }
        return sum;
    }
}


public class ThreadCallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        Future<Integer> f1 = pool.submit(new MyCallable(100));
        Future<Integer> f2= pool.submit(new MyCallable(200));
        System.out.println(f1.get());
        System.out.println(f2.get());
        pool.shutdown();
    }
}

五、匿名内部类方式使用多线程

在现实使用中,有时候没必要新建一个实现Runnable接口的类来创建线程,我们可能把线程用完就丢了,这样就用到了匿名内部类方式的线程。

代码示意:

/*
 * 匿名内部类的格式:
 *      new 类名或者接口名() {
 *          重写方法;
 *      };
 *      本质:是该类或者接口的子类对象。
 */
public class ThreadNimingDemo {
    public static void main(String[] args) {
    // 继承Thread类来实现多线程
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+"---"+i);
                }
            }
        }.start();
        // 实现Runnable接口来实现多线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+"---"+i);
                }
            }
        }){}.start();

        //高难度的错误示范
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("hello"+"---"+i);
                }
            }
        }){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("word"+"---"+i);
                }
            }
        }.start();//执行的是word,这个算是面试题吧,但是这是错误用法,现实中不会出现的。
    }
}

六、多线程总结:

在多线程的面试中经常会问到这些问题:

1:多线程有几种实现方案,分别是哪几种?

两种。

继承Thread类
实现Runnable接口

扩展一种:实现Callable接口。这个得和线程池结合。

2:同步有几种方式,分别是什么?

两种。

同步代码块

同步方法

3:启动一个线程是run()还是start()?它们的区别?

start();

run():封装了被线程执行的代码,直接调用仅仅是普通方法的调用
start():启动线程,并由JVM自动调用run()方法

4:sleep()和wait()方法的区别

sleep():必须指定时间;不释放锁。

wait():可以不指定时间,也可以指定时间;释放锁。

5:为什么wait(),notify(),notifyAll()等方法都定义在Object类中

因为这些方法的调用是依赖于锁对象的,而同步代码块的锁对象是任意锁。
而Object代码任意的对象,所以,定义在这里面。

6:线程的生命周期图

新建 -- 就绪 -- 运行 -- 死亡

新建 -- 就绪 -- 运行 -- 阻塞 -- 就绪 -- 运行 -- 死亡

建议:画图解释。

此次线程的学习,只是简单的涉及,线程间的通信、线程池等并没有深入探究。在经后实际项目的高并发的解决措施中再做详细讲述。此次回顾只是为下一步高并发的研究做基础准备。

此次笔记略显粗糙,欢迎批评指正,互相学习。

源码码云地址:
https://gitee.com/stefanpy/java

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容

  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    小徐andorid阅读 2,783评论 3 53
  •   一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺...
    OmaiMoon阅读 1,630评论 0 12
  • 1. 多线程概述 1.1 多线程引入 由上图中程序的调用流程可知,这个程序只有一个执行流程,所以这样的程序就是单线...
    JackChen1024阅读 380评论 0 1
  • 姓名:张汉超 公司:东莞耀升机电有限公司 组别:4月25-27日六项精进245期学员 【日精进打卡第157天】 【...
    张汉超阅读 144评论 0 0
  • 欢迎Follow我的GitHub, 关注我的简书. 其余参考Android目录. 本文的合集已经编著成书,高级An...
    SpikeKing阅读 2,347评论 5 18